package Managers;

import Entities.Category;
import Tools.DBTools;
import Tools.DatabaseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.XMLResource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by Oliver Mrazik on 5. 6. 2014.
 * author: Oliver Mrazik
 * version: 2014-06-05
 */
public class CategoryManagerImpl implements CategoryManager{

    public static final Logger logger = LoggerFactory.getLogger(CategoryManagerImpl.class);

    private String collection;
    private String login;
    private String pass;

    /**
     * Constructor which initialize database and collection
     * @param collection collection to initialize
     * @param name login to database
     * @param password password for database
     * @throws Tools.DatabaseException when error occurs while setting up driver
     */
    public CategoryManagerImpl(String collection, String name, String password) throws DatabaseException {
        try {
            String driver = "org.exist.xmldb.DatabaseImpl";
            Class cl = Class.forName(driver);
            Database database = (Database) cl.newInstance();
            database.setProperty("create-database", "true");
            DatabaseManager.registerDatabase(database);

            this.collection = collection;
            this.login = name;
            this.pass = password;
        } catch (Exception ex) {
            logger.warn("Error initializing database connection.", ex);
            throw new DatabaseException("Error initializing database connection.", ex);
        }
    }

    /**
     * Method for access to database and return selected collection.
     * @return selected collection
     * @throws Tools.DatabaseException when error occurs when getting collection.
     */
    private Collection getColl() throws DatabaseException {
        try {
            String URI = "xmldb:exist://localhost:8899/exist/xmlrpc";
            //String URI = "xmldb:exist://localhost:8080/exist/xmlrpc";
            return DatabaseManager.getCollection(URI + this.collection, login, pass);
        } catch (XMLDBException ex) {
            logger.debug("Get collection error");
            throw new DatabaseException(ex);
        }
    }

    @Override
    public void createCategory(Category category) throws DatabaseException {
        //------------------------------------------------------------------------------------------------------------//
        //test for category attributes
        if (category == null) {
            logger.debug("Category is null.");
            throw new NullPointerException("Object category is null.");
        }

        if (category.getId() != null) {
            logger.debug("Category id should be null.");
            throw new DatabaseException("Category id should be null.");
        }

        if (category.getName() == null || category.getName().equals("")) {
            logger.debug("Category name is null or empty.");
            throw new IllegalArgumentException("Category name is null or empty.");
        }

        if (checkNames(category.getName())) {
            logger.warn("Category with name \"" + category.getName() + "\" already exist.");
            throw new IllegalArgumentException("Category with name \"" + category.getName() + "\" already exist.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //get collection with categories
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //create XML file & add it to database
        XMLResource resource = null;
        try {
            //--------------------------------------------------------------------------------------------------------//
            //get new id for category
            try {
                category.setId(col.createId());
            } catch (XMLDBException ex) {
                logger.warn("Error creating new id for category.", ex);
                throw new DatabaseException("Error creating new id for category.", ex);
            }
            //--------------------------------------------------------------------------------------------------------//

            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();

            Document doc = docBuilder.newDocument();
            Element root = doc.createElement("category");
            doc.appendChild(root);

            Element mediums = doc.createElement("mediums");
            root.appendChild(mediums);

            root.setAttribute("name", category.getName());

            resource = (XMLResource) col.createResource(category.getId(), "XMLResource");
            resource.setContentAsDOM(doc);
            col.storeResource(resource);
            logger.info("Resource successfully added to database.");
            //------------------------------------------------------------------------------------------------------------//

        } catch (ParserConfigurationException | XMLDBException ex) {
            logger.debug("Document creation error.", ex);
            throw new DatabaseException("Document creation error.", ex);
        } finally {
            DBTools.resourceClose(resource);
            DBTools.collectionClose(col);
        }
    }

    @Override
    public void deleteCategory(Category category) throws DatabaseException {
        //------------------------------------------------------------------------------------------------------------//
        //test for category attributes
        testAttributes(category);

        if (category.getMediums().size() != 0) {
            logger.debug("Category is not empty");
            throw new DatabaseException("Category is not empty");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //get collection with categories
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //delete resource
        XMLResource resource = null;
        try {
            resource = (XMLResource) col.getResource(category.getId());
            if (resource == null) {
                logger.info("Category \"" + category.getName() + "\" is not in database.");
            } else {
                col.removeResource(resource);
            }
        } catch (XMLDBException ex) {
            logger.debug("Error deleting resource." ,ex);
            throw new DatabaseException("Error deleting resource.", ex);
        } finally {
            DBTools.resourceClose(resource);
            DBTools.collectionClose(col);
        }
        //------------------------------------------------------------------------------------------------------------//
    }

    @Override
    public void renameCategory(Category category, String newName) throws DatabaseException {
    //------------------------------------------------------------------------------------------------------------//
        //test for category attributes
        testAttributes(category);

        if (newName == null) {
            logger.debug("New name for category is null.");
            throw new DatabaseException("New name for category is null.");
        }

        if (newName.isEmpty() || newName.equals("")) {
            logger.debug("New name for category is empty");
            throw new DatabaseException("New name for category is empty");
        }

        if (checkNames(newName)) {
            logger.warn("Category with name \"" + category.getName() + "\" already exist.");
            throw new IllegalArgumentException("Category with name \"" + category.getName() + "\" already exist.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //get collection with categories
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //rename existing category
        XMLResource resource = null;
        try {
            resource = (XMLResource) col.getResource(category.getId());
            if (resource == null) {
                // this should never happen!
                logger.debug("Category with name \"" + category.getName() + "\" is not in database.");
            } else {
                Document doc = (Document) resource.getContentAsDOM();
                //old name -> new name
                doc.getFirstChild().getAttributes().getNamedItem("name").setNodeValue(newName);

                resource.setContentAsDOM(doc);
                col.storeResource(resource);
                logger.info("Resource successfully updated.");
            }

        } catch (XMLDBException ex) {
            logger.debug("Document updating error.", ex);
            throw new DatabaseException("Document updating error.", ex);
        } finally {
            DBTools.resourceClose(resource);
            DBTools.collectionClose(col);
        }
        //------------------------------------------------------------------------------------------------------------//
    }

    @Override
    public Category getCategoryById(String id) throws DatabaseException {

        if (id == null) {
            logger.debug("id is null");
            throw new NullPointerException("Id is null.");
        }

        if (id.isEmpty() || id.equals("")) {
            logger.debug("id is empty");
            throw new IllegalArgumentException("Id is empty.");
        }

        //------------------------------------------------------------------------------------------------------------//
        //get collection with categories
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //find and create new category entity
        XMLResource resource = null;
        Category ret = new Category();
        try {
            resource = (XMLResource) col.getResource(id);
            if (resource == null) {
                logger.info("Category with id \"" + id + "\" is not in database.");
                return null;
            }
            Document doc = (Document) resource.getContentAsDOM();

            //set id
            ret.setId(id);

            //set name
            ret.setName(doc.getFirstChild().getAttributes().getNamedItem("name").getNodeValue());

            //set mediums
            NodeList mediums = doc.getElementsByTagName("medium");
            List<String> mediumList = new ArrayList<>();

            for (int i = 0; i < mediums.getLength(); i++) {
                mediumList.add(mediums.item(i).getTextContent());
            }

            ret.setMediums(mediumList);

        } catch (XMLDBException ex) {
            logger.debug("Error creating new Category entity", ex);
            throw new DatabaseException("Error creating new Category entity", ex);
        } finally {
            DBTools.resourceClose(resource);
            DBTools.collectionClose(col);
        }
        //------------------------------------------------------------------------------------------------------------//
        return ret;
    }

    @Override
    public Category getCategoryByName(String name) throws DatabaseException {

        if (name == null) {
            logger.debug("Name is null");
            throw new NullPointerException("Name is null.");
        }

        if (name.isEmpty() || name.equals("")) {
            logger.debug("Name is empty");
            throw new IllegalArgumentException("Name is empty.");
        }

        for (Category category : listAllCategories()) {
            if (category.getName().equals(name)) {
                return category;
            }
        }

        return null;
    }

    @Override
    public List<Category> listAllCategories() throws DatabaseException {

        List<Category> result = new ArrayList<>();

        //------------------------------------------------------------------------------------------------------------//
        //get collection with category
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //gathering categories
        try {
            String[] resources = col.listResources();
            for (int i = 0; i < col.getResourceCount(); i++) {
                //resources[i] = resources[i].substring(0, resources[i].length() - 4);
                result.add(getCategoryById(resources[i]));
            }
        } catch(XMLDBException ex) {
            logger.debug("Error listing all categories", ex);
            throw new DatabaseException("Error listing all categories", ex);
        } finally {
            DBTools.collectionClose(col);
        }
        return result;
        //------------------------------------------------------------------------------------------------------------//
    }

    @Override
    public Document getCategoryAsXMLDocument(String id) throws DatabaseException {
        //------------------------------------------------------------------------------------------------------------//
        //get collection with category
        Collection col = this.getColl();
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //creating new document
        XMLResource resource = null;
        Document doc = null;
        try {
            resource = (XMLResource) col.getResource(id);
            if (resource == null) {
                logger.info("Category with id \"" + id + "\" is not in database.");
                return null;
            }
            doc = (Document) resource.getContentAsDOM();
        } catch (XMLDBException ex) {
            logger.debug("Error creating XMLDocument from category entity", ex);
            throw new DatabaseException("Error creating XMLDocument from category entity", ex);
        } finally {
            DBTools.resourceClose(resource);
            DBTools.collectionClose(col);
        }

        return doc;
    }

    /**
     * Checks if category name already exist.
     * @param newName tested name
     * @return true if name exist, false if not
     * @throws DatabaseException when fetching all categories gets error.
     */
    private boolean checkNames(String newName) throws DatabaseException {
        List<String> names = new ArrayList<>();
        for (Category category : listAllCategories()) {
            names.add(category.getName());
        }
        return names.contains(newName);
    }

    /**
     * Method test attributes of category.
     * @param category tested category
     */
    private void testAttributes(Category category) {

        if (category == null) {
            logger.debug("Category is null.");
            throw new NullPointerException("Object category is null.");
        }

        if (category.getId() == null) {
            logger.debug("Category id is null.");
            throw new NullPointerException("Category id is null.");
        }
    }
}
